package com.pavlinic.mylisp.specialForms;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import com.pavlinic.mylisp.EvaluationContext;
import com.pavlinic.mylisp.Expression;
import com.pavlinic.mylisp.ListExpression;
import com.pavlinic.mylisp.NullExpression;
import com.pavlinic.mylisp.SymbolExpression;
import com.pavlinic.mylisp.ValueExpression;

public class Dot {
  public static Expression evaluateDot(ListExpression listExpression,
      EvaluationContext context) {
    // Should we be evaluating these? or just using them? What if we want to
    // pass in a symbol bound
    // to a class name?
    final String className = ((SymbolExpression) listExpression.get(1))
        .getValue();
    final String methodName = ((SymbolExpression) listExpression.get(2))
        .getValue();
    final ListExpression parameters = evaluateParameters(
        getParameterList(listExpression), context);

    if (parameters.size() == 0) {
      final Field field = findField(className, methodName);
      if (field != null) {
        try {
          return returnValue(field.get(null));
        } catch (IllegalArgumentException e) {
          throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
          throw new RuntimeException(e);
        }
      }
    }

    try {
      final Method method = findMethod(className, methodName, parameters);
      if (method == null) {
        throw new RuntimeException("method " + className + "." + methodName + " not found");
      }
      final Object[] values = convertToValues(parameters);
      final Object result;
      try {
    	  result = method.invoke(null, values);
      } catch (IllegalArgumentException e) {
    	  e.printStackTrace();
    	  throw e;
      }
      // In this case, we have called something that actually manipulates
      // expression structures, which is a special case
      return returnValue(result);
    } catch (SecurityException e) {
      throw new RuntimeException(e);
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    } catch (IllegalArgumentException e) {
      throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    } catch (InvocationTargetException e) {
      throw new RuntimeException(e);
    }
  }

  private static Field findField(final String className, final String methodName) {
    try {
      return Class.forName(className).getField(methodName);
    } catch (SecurityException e) {
      throw new RuntimeException(e);
    } catch (NoSuchFieldException e) {
      return null;
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
  }

  private static Expression returnValue(final Object result) {
    if (result == null) {
      return NullExpression.INSTANCE;
    }
    if (result instanceof Expression) {
      return (Expression) result;
    }
    return new ValueExpression(result);
  }

  private static ListExpression getParameterList(ListExpression listExpression) {
    final ListExpression params = listExpression.rest().rest().rest();
    return params;
  }

  private static ListExpression evaluateParameters(ListExpression arguments,
      EvaluationContext context) {
    final ListExpression argumentValues = new ListExpression();

    for (int i = 0; i < arguments.size(); i++) {
      argumentValues.add(arguments.get(i).evaluate(context));
    }

    return argumentValues;
  }

  private static Object[] convertToValues(ListExpression parameters) {
    final List<Object> values = new ArrayList<Object>();
    for (int i = 0; i < parameters.size(); i++) {
      final Expression expression = parameters.get(i);
      values.add(expression.getValue());
    }
    return values.toArray(new Object[0]);
  }

  static Method findMethod(final String className, final String methodName,
      final ListExpression parameters) throws ClassNotFoundException {
    final Class<?> clazz = Class.forName(className);
    final Class<?>[] types = convertToTypes(parameters);
    return findMethodByTypes(methodName, clazz, types);
  }

  private static Method findMethodByTypes(final String methodName,
      final Class<?> clazz, final Class<?>[] types) {
    try {
      // Try an exact match first
      return clazz.getMethod(methodName, types);
    } catch (NoSuchMethodException e) {
      for (Method method : clazz.getMethods()) {
        if (method.getName().equals(methodName)) {
          final Class<?>[] currentParameterTypes = method.getParameterTypes();
          if (types.length == currentParameterTypes.length) {
            boolean found = true;
            for (int i = 0; i < types.length; i++) {
              if (!types[i].isAssignableFrom(types[i])) {
                found = false;
              }
            }
            if (found) {
              return method;
            }
          }
        }
      }
      return null;
    }
  }

  private static Class<?>[] convertToTypes(ListExpression parameters) {
    final List<Class<?>> types = new ArrayList<Class<?>>();
    for (int i = 0; i < parameters.size(); i++) {
      final Expression expression = parameters.get(i);
      if (expression.equals(NullExpression.INSTANCE)) {
        types.add(Object.class);
      } else {
        types.add(((ValueExpression) expression).getType());
      }
    }
    return types.toArray(new Class<?>[0]);
  }
}
